/*!
 * \file app_impl.cpp - see common.hpp for copyright info.
 */

#include    "common.hpp"
#define     FONT_PATH     "./gfx/font/gamefont.tga"

/*! @brief The virtual width of the display after accounting for the aspect ratio
 *  of the physical one - please see APP::setup for details about widescreen handling.
 */
float   APP::effective_width;
bool    APP::perspective_mode;  //!< Stores whether we're currently in perspective or orthographic projection.
GLuint  APP::game_font;         //!< A default font, loaded in APP::setup().
char    APP::user_home_path[HOME_PATH_LENGTH];
float   APP::aspect_ratio;

/*! @{
 * @brief Performance monitoring and timing-management stuff. Please don't
 * remove the 'volatile' unless you know what you're doing.
 */
volatile int     APP::frames_per_sec;
volatile int     APP::frames_this_sec;
volatile bool    APP::time_for_logic;
/*! @} */

/*! @{
 * @brief These store what way the camera is currently facing and are used to help
 * the sprite- and text-drawing functions 'de-project' the modelview matrix and always
 * face the camera.
 */
float APP::x_rot;
float APP::y_rot;
/*! @} */

/******************************************************************************/
/*! @brief Switch to a perspective projection. Most 3D drawing should be done
 * in this mode.
 */
void APP::to_perspective()
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    glFrustumf(-1.0f * APP::aspect_ratio, 1.0f * APP::aspect_ratio,
                -1.0f, 1.0f,
                2.0f, APP::FAR_CLIP_DEPTH);

    glEnable(GL_DEPTH_TEST);
    glMatrixMode(GL_MODELVIEW);

    APP::perspective_mode = true;
}

/******************************************************************************/
/*! @brief Switch to an orthographic projection and settings useful for drawing
 * dialog boxes, a HUD, a 2D-game, etc.
 *
 * @note the top left is the origin.
 */
void APP::to_ortho()
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    glOrthof(0.0f, (float)APP::DISPLAY_HEIGHT * APP::aspect_ratio,
            (float)APP::DISPLAY_HEIGHT, 0.0f,
            -10.0f, 100.0f);

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    APP::perspective_mode = false;
}

/******************************************************************************/
/*! @brief Stupid but effective replacement for gluLookAt().
 */
void APP::look_at(GLfloat cam_x, GLfloat cam_y, GLfloat cam_z,
                GLfloat dest_x, GLfloat dest_y, GLfloat dest_z)
{
    float x_dist = dest_x - cam_x;
    float y_dist = dest_y - cam_y;
    float z_dist = dest_z - cam_z;

    APP::x_rot = atan2(-x_dist, -z_dist);
    APP::y_rot = atan2( y_dist,hypotf(x_dist, z_dist));

    glRotatef(TO_DEGREES(-APP::x_rot), 1, 0, 0);
    glRotatef(TO_DEGREES(-APP::y_rot), 0, 1, 0);

    glTranslatef(-cam_x,-cam_y,-cam_z);
}

/******************************************************************************/

#define GLYPHS_PER_ROW          16
#define GLYPH_WIDTH             0.0625f
#define GLYPH_HEIGHT            0.125f
#define GLYPH_SCREEN_WIDTH      16.0f
#define FIRST_GLYPH             32
#define PRINTF_BUF_LENGTH       512

/******************************************************************************/
/*!
 * @brief Display a string at the specified location, in the specified font.
 *
 * @note Can't be used to print strings longer than 512 characters - if you
 * need this, you'll have to break your string into chunks.
 *
 * @note There isn't any support for characters outside of ASCII currently;
 * if someone reading this has an idea of how to extend it for CKJV and
 * accent support, let me know!
 */
void APP::gfx_printf(GLuint font_tex, GLfloat x, GLfloat y, GLfloat z, GLfloat size,
    GLfloat spacing, const char *str, ...)
{
    int         row;
    int         col;
    int         index;
    VERTEX      verts[4];
    TEX_COORD   uvs[4];
    va_list     variadic_args;
    char        buf[PRINTF_BUF_LENGTH];
    GLfloat     centering_offset;

    // Null string? Nothing to do here.
    if (str == NULL) return;

    // Process the string's format specifiers and added args...
    va_start(variadic_args, str);
    snprintf(buf, PRINTF_BUF_LENGTH, str, variadic_args);
    va_end(variadic_args);

    // Set up all gl state in preparation for drawing.
    glBindTexture(GL_TEXTURE_2D, font_tex);
    glDisableClientState(GL_COLOR_ARRAY);
    glEnable(GL_TEXTURE_2D);
    glTexCoordPointer(2,GL_FLOAT,0,uvs);
    glVertexPointer(3,GL_FLOAT,0,verts);

    // Perspective camera?
    if (APP::in_perspective_mode())
    {
        // Yes. Centre on the given point, and always turn to
        // face camera.
        glPushMatrix();
        glTranslatef(x, y, z);
        glRotatef(TO_DEGREES(APP::y_rot), 0, 1, 0);
        glRotatef(TO_DEGREES(APP::x_rot), 1, 0, 0);

        centering_offset = (strlen(str) / 2.0) * (size + spacing);
    }

    for(index = 0; index < strlen(str); index++)
    {
        // Navigate to the glyph in the texture.
        row = (buf[index] - FIRST_GLYPH) / GLYPHS_PER_ROW;
        col = (buf[index] - FIRST_GLYPH) % GLYPHS_PER_ROW;

        // Are we in 3d?
        if (APP::in_perspective_mode())
        {
            // Yes; draw the glyphs such that the string is centered
            // on the given point and rotated to face the camera.
            //
            // This looks wrong, but it isn't: because we each glyph
            // as having its origin the top left(opengl sort of 'expects'
            // the bottom left by default), each glyph in 3d has to be
            // drawn upside down to fix this up, thus the weird UV coords.
            verts[0].x = -centering_offset + (index * (size + spacing));
            verts[0].y = 0;
            verts[0].z = 0;

            uvs[0].u = col * GLYPH_WIDTH;
            uvs[0].v = (row + 1) * GLYPH_HEIGHT;

            verts[1].x = verts[0].x + size;
            verts[1].y = 0;
            verts[1].z = 0;

            uvs[1].u = (col + 1) * GLYPH_WIDTH;
            uvs[1].v = (row + 1) * GLYPH_HEIGHT;

            verts[2].x = verts[0].x + size;
            verts[2].y = size;
            verts[2].z = 0;

            uvs[2].u = (col + 1) * GLYPH_WIDTH;
            uvs[2].v = row * GLYPH_HEIGHT;

            verts[3].x = verts[0].x;
            verts[3].y = size;
            verts[3].z = 0;

            uvs[3].u = col * GLYPH_WIDTH;
            uvs[3].v = row * GLYPH_HEIGHT;
        }
        else
        {
            // No. Draw these as plain old 2D quads.
            verts[0].x = x  + (index * (size + spacing));
            verts[0].y = y;
            verts[0].z = z;

            uvs[0].u = col * GLYPH_WIDTH;
            uvs[0].v = row * GLYPH_HEIGHT;

            verts[1].x = verts[0].x + size;
            verts[1].y = y;
            verts[1].z = z;

            uvs[1].u = (col + 1) * GLYPH_WIDTH;
            uvs[1].v = row * GLYPH_HEIGHT;

            verts[2].x = verts[0].x + size;
            verts[2].y = y + size;
            verts[2].z = z;

            uvs[2].u = (col + 1) * GLYPH_WIDTH;
            uvs[2].v = (row + 1) * GLYPH_HEIGHT;

            verts[3].x = verts[0].x;
            verts[3].y = y + size;
            verts[3].z = z;

            uvs[3].u = col * GLYPH_WIDTH;
            uvs[3].v = (row + 1) * GLYPH_HEIGHT;
        }

        glDrawArrays(GL_TRIANGLE_FAN,0,4);
    }

    // If we had to modify this, set it back to what the caller's
    // expecting...
    if (APP::perspective_mode)
        glPopMatrix();
}

/*****************************************************************************/
/*!
 * @brief Draw a sprite centered at <i>x, y</i> and <i>w, h</i> units across.
 *
 * @note As for why not use the point sprites extension, not every machine
 * supports the same range of sizes, and they're always square...
 *
 * @warning Sprites are drawn by their centre point, even in ortho mode.
 */
void APP::draw_sprite(GLuint sprite_tex, float x, float y, float z, float w, float h)
{
    VERTEX verts[4];
    TEX_COORD uvs[4];

    // Set up all gl state in preparation for drawing.
    glBindTexture(GL_TEXTURE_2D, sprite_tex);
    glDisableClientState(GL_COLOR_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glEnable(GL_TEXTURE_2D);
    glTexCoordPointer(2,GL_FLOAT,0,uvs);
    glVertexPointer(3,GL_FLOAT,0,verts);

    uvs[0].u = 0;
    uvs[0].v = 0;

    uvs[1].u = 1;
    uvs[1].v = 0;

    uvs[2].u = 1;
    uvs[2].v = 1;

    uvs[3].u = 0;
    uvs[3].v = 1;

    // Perspective camera?
    if (APP::in_perspective_mode())
    {
        // Yes. Centre on the given point, and always turn to
        // face camera.
        glPushMatrix();
        glTranslatef(x, y, z);
        glRotatef(TO_DEGREES(APP::y_rot), 0, 1, 0);
        glRotatef(TO_DEGREES(APP::x_rot), 1, 0, 0);

        verts[0].x = -w / 2.0;
        verts[0].y =  h / 2.0;
        verts[0].z =  0;

        verts[1].x =  w / 2.0;
        verts[1].y =  h / 2.0;
        verts[1].z =  0;

        verts[2].x =  w / 2.0;
        verts[2].y = -h / 2.0;
        verts[2].z =  0;

        verts[3].x = -w / 2.0;
        verts[3].y = -h / 2.0;
        verts[3].z =  0;
    }
    else
    {
        // Orthographic projection, just draw it to screen coords.
        verts[0].x = x - (w / 2.0);
        verts[0].y = y - (h / 2.0);
        verts[0].z = z;

        verts[1].x = x + (w / 2.0);
        verts[1].y = y - (h / 2.0);
        verts[1].z = z;

        verts[2].x = x + (w / 2.0);
        verts[2].y = y + (h / 2.0);
        verts[2].z = z;

        verts[3].x = x - (w / 2.0);
        verts[3].y = y + (h / 2.0);
        verts[3].z = z;
    }

    glDrawArrays(GL_TRIANGLE_FAN,0,4);

    // If we had to modify this, set it back to what the caller's
    // expecting...
    if (APP::perspective_mode)
        glPopMatrix();
}

/*****************************************************************************/
/*!
 * @brief Draw a sprite centered at <i>x, y</i> and <i>size, size</i> units across.
 */
void APP::draw_sprite(GLuint sprite_tex, float x, float y, float z, float size)
{
    APP::draw_sprite(sprite_tex, x, y, z, size, size);
}

/*****************************************************************************/
/*!
 * @brief Returns the virtual width of the display.
 */
float APP::get_effective_width()
{
    return APP::effective_width;
}

/*****************************************************************************/
/*!
 * @brief Returns whether it's time for the next update or not.
 */
bool APP::is_time_for_logic()
{
    return APP::time_for_logic;
}

/*****************************************************************************/
/*!
 * @brief Returns the number of joysticks available to the application.
 */
int APP::get_num_joysticks()
{
    return num_joysticks;
}

/*****************************************************************************/
/*!
 * @brief Loads an image from disk and converts it into a texture. Must be
 * a filetype Allegro already knows about (BMP, TGA, PCX).
 *
 * @note If the image can't be loaded for any reason, a valid-but-deliberately
 *  ugly texture is created, and a warning is printed to stderr.
 *
 * @note Remember to call APP::unload_texture() with the returned texture name
 * or else...
 */
GLuint APP::load_texture(const char *filename)
{
    int format;
    GLuint ret_val;

    BITMAP *b;
    set_color_depth(24);
    set_color_conversion(COLORCONV_KEEP_ALPHA);

    // Image file existed on disk?
    if (exists(filename))
    {
        // Yes, load it.
        b = load_bitmap(filename,NULL);
    }
    else
    {
        // No - default to ugly bright green/
        DUH_WHERE_AM_I("missing image: %s", filename);
        b = create_bitmap_ex(24,1,1);
        putpixel(b,0,0,makecol24(0,255,0));
    }

    switch(bitmap_color_depth(b))
    {
        case 32:    format = GL_RGBA;
                    break;

        case 24:    format = GL_RGB;
                    break;

        case 16:    format = GL_RGB;
                    break;

        case 8:     format = GL_LUMINANCE;
                    break;

        default:   format = GL_RGB;
    }

    // Copy the bitmap from the app over to texture memory...
    glGenTextures(1, &ret_val);
    glBindTexture(GL_TEXTURE_2D, ret_val);
    glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexImage2D(GL_TEXTURE_2D, 0, format, b->w, b->h, 0, format, GL_UNSIGNED_BYTE, b->line[0]);

    destroy_bitmap(b);

    return ret_val;
}

/*****************************************************************************/
/*!
 * @brief Unload the texture passed in the argument.
 *
 * Exists purely to make the API symmetrical; it's really just a wrapper for
 * glDeleteTextures().
 */
void APP::unload_texture(GLuint *tex)
{
    glDeleteTextures(1, tex);
}

/*****************************************************************************/
/*! @brief Returns the default font.
 */
GLuint APP::get_game_font()
{
    return APP::game_font;
}

/*****************************************************************************/
/*! @brief Returns the default font.
 */
void APP::repaint()
{
    // This calls glFlush(), so no need to call it ourselves.
    allegro_gl_flip();
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    APP::frames_this_sec++;
}

/******************************************************************************/
/*!
 * @brief Reports how many frames we were able to draw in the last second.
 * It is bound to a timer and should never be called manually.
 */
void APP::sample_framerate()
{
    APP::frames_per_sec  = APP::frames_this_sec;
    APP::frames_this_sec = 0;
}
END_OF_FUNCTION(sample_framerate);

/******************************************************************************/
/*!
 * @brief Advances the game logic at least one tick, or more if a repaint takes too
 * long. Called automatically as a timer task, should never be called manually.
 *
 * To use this:
 * In the game loop, we have something like
 * while(!key[KEY_ESC])
 * {
 *      if(time_for_logic)
 *      {
 *          do_something_awesome();
 *          draw_something_awesome();
 *          time_for_logic = false;
 *      }
 *      else
 *      {
 *          rest(1); // don't hog the cpu when not needed
 *      }
 * }
 */
void APP::heartbeat()
{
    time_for_logic = true;
}
END_OF_FUNCTION(heartbeat);

/******************************************************************************/
/*!
 * @brief Tell the rest of the app we're done with the current iteration of the
 * game loop; should be called once per cycle.
 */
void APP::logic_done()
{
    time_for_logic = false;
}

/******************************************************************************/
/*!
 * @brief Returns true if we're in perspective mode currently, or false otherwise.
 */
bool APP::in_perspective_mode()
{
    return perspective_mode;
}

/******************************************************************************/
/*!
 * @brief Prepare the sound, music, input, timer and graphics subsystems for use.
 *
 * @warning This <i>must</i> be called before anything that loads images, sounds,
 * etc, or your program will probably crash.
 */
void APP::setup(bool use_fullscreen)
{
    // Try to get the current user's home dir (for savegames, etc.)
    // I hope this doesn't end up on tdwtf, but I have no idea what else to do...
#ifdef  __linux__
    strncpy(APP::user_home_path, getenv("HOME"), HOME_PATH_LENGTH - 1);
#else
#ifdef _WIN32
    strncpy(APP::user_home_path, getenv("USERPROFILE"), HOME_PATH_LENGTH - 1);
#else
#ifdef __APPLE__
        struct passwd* pwd = getpwuid(getuid());
        if (pwd)
            strncpy(APP::user_home_path, pwd->pw_dir, HOME_PATH_LENGTH - 1);
        else
        {
            snprintf(APP::user_home_path, "/dev/null", HOME_PATH_LENGTH - 1);
            DUH_WHERE_AM_I("couldn't get the current user's home directory!");
        }
#endif
#endif
#endif

    allegro_init();
    install_allegro_gl();
    install_sound(DIGI_AUTODETECT, MIDI_NONE, NULL);
    install_keyboard();
    install_joystick(JOY_TYPE_AUTODETECT);

    // Set up the display.
    // Compute the correct aspect ratio for the user's display.
    int width_tmp;
    int height_tmp;

    if((!use_fullscreen) || (get_desktop_resolution(&width_tmp, &height_tmp) != 0))
    {
        width_tmp  = APP::DISPLAY_WIDTH;
        height_tmp = APP::DISPLAY_HEIGHT;
    }

    APP::aspect_ratio = (float)width_tmp / (float)height_tmp;
    APP::effective_width = APP::DISPLAY_WIDTH *
        (APP::aspect_ratio / ((float)APP::DISPLAY_WIDTH / (float)APP::DISPLAY_HEIGHT));

    // Choose a colour depth.
    if(desktop_color_depth() != 0)
        set_color_depth(desktop_color_depth());
    else
        set_color_depth(16);

    // Suggest a good screen mode for OpenGL.
    allegro_gl_set(AGL_Z_DEPTH, get_color_depth());
    allegro_gl_set(AGL_COLOR_DEPTH, get_color_depth());
    allegro_gl_set(AGL_FULLSCREEN, use_fullscreen);
    allegro_gl_set(AGL_DOUBLEBUFFER, TRUE);
    allegro_gl_set(AGL_SUGGEST,
        AGL_FULLSCREEN | AGL_DOUBLEBUFFER | AGL_Z_DEPTH | AGL_COLOR_DEPTH);

    // Set the graphics mode.
    set_gfx_mode(GFX_OPENGL, APP::DISPLAY_WIDTH, APP::DISPLAY_HEIGHT, 0, 0);

    // Set the font
    if(exists(FONT_PATH))
    {
        APP::game_font = APP::load_texture(FONT_PATH);
        // Turn off mipmapping - if we don't, the font will be illegible at smaller sizes.
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    }

    // Set some basic GL settings for speed
    glFogf(GL_FOG_MODE, GL_LINEAR);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
    glHint(GL_FOG_HINT, GL_FASTEST);

    // Set up the heartbeat
    APP::time_for_logic = false;
    LOCK_VARIABLE(APP::time_for_logic);
    LOCK_FUNCTION(APP::heartbeat);
    install_int_ex(APP::heartbeat, BPS_TO_TIMER(APP::UPDATES_PER_SEC));

    // Set up the performance timer
    APP::frames_this_sec = 0;
    APP::frames_per_sec = 0;
    LOCK_VARIABLE(APP::frames_per_sec);
    LOCK_VARIABLE(APP::frames_this_sec);
    LOCK_FUNCTION(APP::sample_framerate);
    install_int_ex(APP::sample_framerate,BPS_TO_TIMER(1));

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glAlphaFunc(GL_GREATER, 0.5);
}

/*****************************************************************************/
/*! @brief Show the number of frames drawn in the last second in the upper
 * left corner.
 *
 * @note For debugging, shouldn't be called in enduser-facing releases.
 * @note Sort of 'expects' to be called from ortho mode.
 */
void APP::show_fps()
{
    if(APP::frames_per_sec < (APP::UPDATES_PER_SEC / 2))
        glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
    else if(APP::frames_per_sec < (APP::UPDATES_PER_SEC / 1.5))
        glColor4f(1.0f, 1.0f, 0.0f, 1.0f);
    else
        glColor4f(0.0f, 1.0f, 0.0f, 1.0f);

    APP::gfx_printf(APP::get_game_font(), 8, 8, 0, 16, 0, "FPS: %03d", APP::frames_per_sec);
}

/******************************************************************************/
/*! @brief Turn off sound, music, timer, input and graphics subsystems and
 * cleans up. Should be called right before the end of main().
 */
void APP::shutdown(void)
{
    DUH_WHERE_AM_I("Exiting.\n");
    clear_keybuf();
    remove_int(APP::heartbeat);
    remove_int(APP::sample_framerate);
    APP::unload_texture(&APP::game_font);
    set_gfx_mode(GFX_TEXT,0,0,0,0);
}
